/*******************************************************************************
 * Copyright (c) 2003, 2015 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.internal.progress;

import java.time.Duration;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.eclipse.rap.rwt.SingletonUtil;
import org.eclipse.rap.rwt.internal.lifecycle.LifeCycleUtil;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkbenchPreferenceConstants;
import org.eclipse.ui.internal.util.PrefUtil;

/**
 * The ProgressViewUpdater is the singleton that updates viewers.
 */
//RAP [fappel]: ProgressViewUpdater needs to be a singleton per session
//class ProgressViewUpdater implements IJobProgressManagerListener {
//
// private static ProgressViewUpdater singleton;
public class ProgressViewUpdater implements IJobProgressManagerListener {

	// RAP [fappel]: This class will be instanciated using the
	// SessionSingletonUtil#getInstance(class) method to have a
	// replacement for the class variable holding the singleton in
	// RCP.
	public final static class ProgressViewUpdaterHolder {
		private ProgressViewUpdaterHolder() {
			// prevent from instance creation
		}

		// this is the reference to the actual session singleton instance
		public ProgressViewUpdater singleton;
	}

	private Set<IProgressUpdateCollector> collectors;

    UpdatesInfo currentInfo = new UpdatesInfo();

    boolean debug;

    // RAP [fappel]:
    private Display display;

	Throttler throttledUpdate;

    /**
     * The UpdatesInfo is a private class for keeping track of the updates
     * required.
     */
	static class UpdatesInfo {

		Collection<JobTreeElement> additions = new LinkedHashSet<>();

		Collection<JobTreeElement> deletions = new LinkedHashSet<>();

		Collection<JobTreeElement> refreshes = new LinkedHashSet<>();

		volatile boolean updateAll;

        private UpdatesInfo() {
            //Create a new instance of the info
        }

        /**
		 * Add an add update
		 *
		 * @param addition
		 */
		synchronized void add(JobTreeElement addition) {
            additions.add(addition);
        }

        /**
		 * Add a remove update
		 *
		 * @param removal
		 */
		synchronized void remove(JobTreeElement removal) {
            deletions.add(removal);
        }

        /**
		 * Add a refresh update
		 *
		 * @param refresh
		 */
		synchronized void refresh(JobTreeElement refresh) {
            refreshes.add(refresh);
        }

        /**
         * Reset the caches after completion of an update.
         */
		synchronized void reset() {
            additions.clear();
            deletions.clear();
            refreshes.clear();
            updateAll = false;
        }

		/**
		 * @return array containing updated, added and deleted items
		 */
		synchronized JobTreeElement[][] processForUpdate() {
			HashSet<JobTreeElement> staleAdditions = new HashSet<>();

			Iterator<JobTreeElement> additionsIterator = additions.iterator();
            while (additionsIterator.hasNext()) {
				JobTreeElement treeElement = additionsIterator
                        .next();
                if (!treeElement.isActive()) {
                    if (deletions.contains(treeElement)) {
						staleAdditions.add(treeElement);
					}
                }
            }

            additions.removeAll(staleAdditions);

			HashSet<JobTreeElement> obsoleteRefresh = new HashSet<>();
			for (JobTreeElement treeElement : refreshes) {
                if (deletions.contains(treeElement)
                        || additions.contains(treeElement)) {
					obsoleteRefresh.add(treeElement);
				}

                //Also check for groups that are being added
               Object parent = treeElement.getParent();
               if(parent != null && (deletions.contains(parent)
                       || additions.contains(parent))){
            	   obsoleteRefresh.add(treeElement);
               }

                if (!treeElement.isActive()) {
                    //If it is done then delete it
                    obsoleteRefresh.add(treeElement);
                    deletions.add(treeElement);
                }
            }

            refreshes.removeAll(obsoleteRefresh);

			JobTreeElement[] updateItems = refreshes.toArray(new JobTreeElement[0]);
			JobTreeElement[] additionItems = additions.toArray(new JobTreeElement[0]);
			JobTreeElement[] deletionItems = deletions.toArray(new JobTreeElement[0]);
			return new JobTreeElement[][] { updateItems, additionItems, deletionItems };
		}

    }

    /**
     * Return a new instance of the receiver.
     *
     * @return ProgressViewUpdater
     */
    static ProgressViewUpdater getSingleton() {
// RAP [fappel]: session aware implementation
//        if (singleton == null) {
//			singleton = new ProgressViewUpdater();
//		}
//        return singleton;
      ProgressViewUpdaterHolder singletonHolder = getSingletonHolder();
      if( singletonHolder.singleton == null ) {
        singletonHolder.singleton = new ProgressViewUpdater();
        singletonHolder.singleton.display = LifeCycleUtil.getSessionDisplay();
			singletonHolder.singleton.throttledUpdate = new Throttler(getSingleton().display, Duration.ofMillis(100),
					singletonHolder.singleton::update);
      }
      return singletonHolder.singleton;
    }

    private static ProgressViewUpdaterHolder getSingletonHolder() {
		return SingletonUtil.getSessionInstance(ProgressViewUpdaterHolder.class);
    }

    /**
	 * Return whether or not there is a singleton for updates to avoid creating
	 * extra listeners.
	 *
	 * @return boolean <code>true</code> if there is already a singleton
	 */
    static boolean hasSingleton() {
// RAP [fappel]:
//      return singleton != null;
		return getSingletonHolder().singleton != null;
    }

    static void clearSingleton() {
// RAP [fappel]:
//        if (singleton != null) {
//			ProgressManager.getInstance().removeListener(singleton);
//		}
//        singleton = null;
      if( hasSingleton() ) {
        ProgressManager.getInstance().removeListener(getSingleton());
        getSingletonHolder().singleton.display = null;
      }
      getSingletonHolder().singleton = null;
    }

    /**
     * Create a new instance of the receiver.
     */
    private ProgressViewUpdater() {
		collectors = new LinkedHashSet<>();
        ProgressManager.getInstance().addListener(this);
		debug =
        	PrefUtil.getAPIPreferenceStore().
        		getBoolean(IWorkbenchPreferenceConstants.SHOW_SYSTEM_JOBS);
    }

    /**
	 * Add the new collector to the list of collectors.
	 *
	 * @param newCollector
	 */
    void addCollector(IProgressUpdateCollector newCollector) {
		collectors.add(newCollector);
    }

    /**
	 * Remove the collector from the list of collectors.
	 *
	 * @param provider
	 */
    void removeCollector(IProgressUpdateCollector provider) {
		collectors.remove(provider);
        //Remove ourselves if there is nothing to update
		if (collectors.isEmpty()) {
			clearSingleton();
		}
    }

	/** Running in UI thread by throttledUpdate */
	private void update() {
		// Abort the update if there isn't anything
		if (collectors.isEmpty()) {
			return;
		}

		if (currentInfo.updateAll) {
			currentInfo.reset();
			for (IProgressUpdateCollector collector : collectors) {
				collector.refresh();
			}

		} else {
			JobTreeElement[][] elements = currentInfo.processForUpdate();

			JobTreeElement[] updateItems = elements[0];
			JobTreeElement[] additionItems = elements[1];
			JobTreeElement[] deletionItems = elements[2];

			currentInfo.reset();

			for (IProgressUpdateCollector collector : collectors) {
				if (updateItems.length > 0) {
					collector.refresh(updateItems);
				}
				if (additionItems.length > 0) {
					collector.add(additionItems);
				}
				if (deletionItems.length > 0) {
					collector.remove(deletionItems);
				}
			}
		}
	}

	@Override
	public void refreshJobInfo(JobInfo info) {
		currentInfo.refresh(info);
        //Add in a 100ms delay so as to keep priority low
		throttledUpdate.throttledExec();

    }

	@Override
	public void refreshGroup(GroupInfo info) {
		currentInfo.refresh(info);
        //Add in a 100ms delay so as to keep priority low
		throttledUpdate.throttledExec();
    }

	@Override
	public void addGroup(GroupInfo info) {
		currentInfo.add(info);
		throttledUpdate.throttledExec();
    }

	@Override
	public void refreshAll() {
		currentInfo.updateAll = true;

        //Add in a 100ms delay so as to keep priority low
		throttledUpdate.throttledExec();
    }

	@Override
	public void addJob(JobInfo info) {
		GroupInfo group = info.getGroupInfo();

		if (group == null) {
			currentInfo.add(info);
		} else {
			currentInfo.refresh(group);
        }
		throttledUpdate.throttledExec();
    }

	@Override
	public void removeJob(JobInfo info) {
		GroupInfo group = info.getGroupInfo();
		if (group == null) {
			currentInfo.remove(info);
		} else {
			currentInfo.refresh(group);
        }
		throttledUpdate.throttledExec();
    }

	@Override
	public void removeGroup(GroupInfo group) {
		currentInfo.remove(group);
		throttledUpdate.throttledExec();
    }

	@Override
	public boolean showsDebug() {
        return debug;
    }

}
